/*
 * File : $HeadURL$
 * Date : $Date: 2011-02-22 16:34:35 +0100 (Di, 22 Feb 2011) $
 * Version: $Revision: 5 $
 * 
 * This file is part of BIDS, the batch image downscaler.
 * 
 * It was part of OpenCms and was taken out and adapted for this project.
 * 
 * Copyright (C) 2011 Michael Moossen
 */

/*
 * File : /usr/local/cvs/opencms/src/org/opencms/loader/CmsImageScaler.java
 * Date : 2009-08-20 11:31:34
 * Version: 1.16
 * 
 * This library is part of OpenCms -
 * the Open Source Content Management System
 * 
 * Copyright (c) 2002 - 2009 Alkacon Software GmbH (http://www.alkacon.com)
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * 
 * For further information about Alkacon Software, please see the
 * company website: http://www.alkacon.com
 * 
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

package net.moossen.bids;

import java.awt.Color;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import com.alkacon.simapi.RenderSettings;
import com.alkacon.simapi.Simapi;
import com.alkacon.simapi.filter.GrayscaleFilter;
import com.alkacon.simapi.filter.ShadowFilter;

/**
 * Creates scaled images, acting as it's own parameter container.
 * 
 * @author Alexander Kandzior
 * @author Michael Moossen
 * 
 * @version 1.16
 * 
 * @since 6.2.0
 */
public class ImageScaler {

    /** The name of the transparent color (for the background image). */
    public static final String COLOR_TRANSPARENT = "transparent";

    /** The name of the grayscale image filter. */
    public static final String FILTER_GRAYSCALE = "grayscale";

    /** The name of the shadow image filter. */
    public static final String FILTER_SHADOW = "shadow";

    /** The supported image filter names. */
    public static final List FILTERS = Arrays.asList(new String[] {
        ImageScaler.FILTER_GRAYSCALE,
        ImageScaler.FILTER_SHADOW});

    /**
     * The (optional) parameter used for sending the scale information of an
     * image in the http request.
     */
    public static final String PARAM_SCALE = "__scale";

    /**
     * The default maximum image size (width * height) to apply image blurring
     * when down scaling (setting this to high may case "out of memory" errors).
     */
    public static final int SCALE_DEFAULT_MAX_BLUR_SIZE = 2500 * 2500;

    /**
     * The default maximum image size (width or height) to allow when up or down
     * scaling an image using request parameters.
     */
    public static final int SCALE_DEFAULT_MAX_SIZE = 1500;

    /**
     * The scaler parameter to indicate the requested image background color (if
     * required).
     */
    public static final String SCALE_PARAM_COLOR = "c";

    /** The scaler parameter to indicate crop height. */
    public static final String SCALE_PARAM_CROP_HEIGHT = "ch";

    /** The scaler parameter to indicate crop width. */
    public static final String SCALE_PARAM_CROP_WIDTH = "cw";

    /** The scaler parameter to indicate crop X coordinate. */
    public static final String SCALE_PARAM_CROP_X = "cx";

    /** The scaler parameter to indicate crop Y coordinate. */
    public static final String SCALE_PARAM_CROP_Y = "cy";

    /** The scaler parameter to indicate the requested image filter. */
    public static final String SCALE_PARAM_FILTER = "f";

    /** The scaler parameter to indicate the requested image height. */
    public static final String SCALE_PARAM_HEIGHT = "h";

    /**
     * The scaler parameter to indicate the requested image position (if
     * required).
     */
    public static final String SCALE_PARAM_POS = "p";

    /**
     * The scaler parameter to indicate to requested image save quality in
     * percent (if applicable, for example used with JPEG images).
     */
    public static final String SCALE_PARAM_QUALITY = "q";

    /**
     * The scaler parameter to indicate to requested
     * <code>{@link java.awt.RenderingHints}</code> settings.
     */
    public static final String SCALE_PARAM_RENDERMODE = "r";

    /** The scaler parameter to indicate the requested scale type. */
    public static final String SCALE_PARAM_TYPE = "t";

    /** The scaler parameter to indicate the requested image width. */
    public static final String SCALE_PARAM_WIDTH = "w";

    /**
     * Returns the color value (<code>{@link Color}</code>) for the given String
     * value.
     * <p>
     * 
     * All parse errors are caught and the given default value is returned in
     * this case.
     * <p>
     * 
     * @param value the value to parse as color
     * @param defaultValue the default value in case of parsing errors
     * 
     * @return the int value for the given parameter value String
     */
    public static Color getColorValue(String value, final Color defaultValue) {

        Color result;
        try {
            final char pre = value.charAt(0);
            if (pre != '#') {
                value = "#" + value;
            }
            result = Color.decode(value);
        } catch (final Exception e) {
            result = defaultValue;
        }
        return result;
    }

    /**
     * Returns the Integer (int) value for the given String value.
     * <p>
     * 
     * All parse errors are caught and the given default value is returned in
     * this case.
     * <p>
     * 
     * @param value the value to parse as int
     * @param defaultValue the default value in case of parsing errors
     * 
     * @return the int value for the given parameter value String
     */
    public static int getIntValue(final String value, final int defaultValue) {

        int result;
        try {
            result = Integer.valueOf(value).intValue();
        } catch (final Exception e) {
            result = defaultValue;
        }
        return result;
    }

    /** The target background color (optional). */
    private Color m_color;

    /** The height for image cropping. */
    private int m_cropHeigt;

    /** The width for image cropping. */
    private int m_cropWidth;

    /** The x coordinate for image cropping. */
    private int m_cropX;

    /** The y coordinate for image cropping. */
    private int m_cropY;

    /** The list of image filter names (Strings) to apply. */
    private List m_filters;

    /** The target height (required). */
    private int m_height;

    /**
     * The maximum image size (width * height) to apply image blurring when down
     * scaling (setting this to high may case "out of memory" errors).
     */
    private int m_maxBlurSize;

    /** The target position (optional). */
    private int m_position;

    /**
     * The target image save quality (if applicable, for example used with JPEG
     * images) (optional).
     */
    private int m_quality;

    /**
     * The image processing renderings hints constant mode indicator (optional).
     */
    private int m_renderMode;

    /** The final (parsed and corrected) scale parameters. */
    private String m_scaleParameters;

    /** The target scale type (optional). */
    private int m_type;

    /** The target width (required). */
    private int m_width;

    /**
     * Creates a new, empty image scaler object.
     * <p>
     */
    public ImageScaler() {

        init();
    }

    /**
     * Creates a new image scaler initialized with the height and width of
     * the given image contained in the byte array.
     * <p>
     * 
     * <b>Please note:</b>The image itself is not stored in the scaler, only the
     * width and height dimensions of the image. To actually scale an image, you
     * need to use <code>{@link #scaleImage(CmsFile)}</code>. This constructor
     * is commonly used only to extract the image dimensions, for example when
     * creating a String value for the
     * <code>{@link CmsPropertyDefinition#PROPERTY_IMAGE_SIZE}</code> property.
     * <p>
     * 
     * In case the byte array can not be decoded to an image, or in case of
     * other errors, <code>{@link #isValid()}</code> will return
     * <code>false</code>.
     * <p>
     * 
     * @param content the image to calculate the dimensions for
     * 
     * @throws Exception
     */
    public ImageScaler(final byte[] content)
    throws Exception {

        init();
        // read the scaled image
        final BufferedImage image = Simapi.read(content);
        m_height = image.getHeight();
        m_width = image.getWidth();
    }

    /**
     * Creates a new image scaler based on the given parameter String.
     * <p>
     * 
     * @param parameters the scale parameters to use
     */
    public ImageScaler(final String parameters) {

        init();
        parseParameters(parameters);
    }

    /**
     * Creates a new image scaler based on the given base scaler and the given
     * width and height.
     * <p>
     * 
     * @param base the base scaler to initialize the values with
     * @param width the width to set for this scaler
     * @param height the height to set for this scaler
     */
    protected ImageScaler(final ImageScaler base, final int width, final int height) {

        initValuesFrom(base);
        setWidth(width);
        setHeight(height);
    }

    /**
     * Adds a filter name to the list of filters that should be applied to the
     * image.
     * <p>
     * 
     * @param filter the filter name to add
     */
    public void addFilter(String filter) {

        filter = filter.trim().toLowerCase();
        if (ImageScaler.FILTERS.contains(filter)) {
            m_filters.add(filter);
        }
    }

    /**
     * @see java.lang.Object#clone()
     */
    @Override
    public Object clone() {

        final ImageScaler clone = new ImageScaler();
        clone.initValuesFrom(this);
        return clone;
    }

    /**
     * Returns the color.
     * <p>
     * 
     * @return the color
     */
    public Color getColor() {

        return m_color;
    }

    /**
     * Returns the color as a String.
     * <p>
     * 
     * @return the color as a String
     */
    public String getColorString() {

        final StringBuffer result = new StringBuffer();
        if (m_color == Simapi.COLOR_TRANSPARENT) {
            result.append(ImageScaler.COLOR_TRANSPARENT);
        } else {
            if (m_color.getRed() < 16) {
                result.append('0');
            }
            result.append(Integer.toString(m_color.getRed(), 16));
            if (m_color.getGreen() < 16) {
                result.append('0');
            }
            result.append(Integer.toString(m_color.getGreen(), 16));
            if (m_color.getBlue() < 16) {
                result.append('0');
            }
            result.append(Integer.toString(m_color.getBlue(), 16));
        }
        return result.toString();
    }

    /**
     * Returns the crop area height.
     * <p>
     * 
     * Use {@link #setCropArea(int, int, int, int)} to set this value.
     * <p>
     * 
     * @return the crop area height
     */
    public int getCropHeigt() {

        return m_cropHeigt;
    }

    /**
     * Returns a new image scaler that is a cropped rescaler from
     * <code>this</code> cropped scaler
     * size to the given target scaler size.
     * <p>
     * 
     * @param target the image scaler that holds the target image dimensions
     * 
     * @return a new image scaler that is a cropped rescaler from
     *         <code>this</code> cropped scaler
     *         size to the given target scaler size
     * 
     * @see #getReScaler(ImageScaler)
     * @see #setCropArea(int, int, int, int)
     */
    public ImageScaler getCropScaler(final ImageScaler target) {

        // first re-scale the image (if required)
        final ImageScaler result = getReScaler(target);
        // now use the crop area from the original
        result.setCropArea(m_cropX, m_cropY, m_cropWidth, m_cropHeigt);
        return result;
    }

    /**
     * Returns the crop area width.
     * <p>
     * 
     * Use {@link #setCropArea(int, int, int, int)} to set this value.
     * <p>
     * 
     * @return the crop area width
     */
    public int getCropWidth() {

        return m_cropWidth;
    }

    /**
     * Returns the crop area X start coordinate.
     * <p>
     * 
     * Use {@link #setCropArea(int, int, int, int)} to set this value.
     * <p>
     * 
     * @return the crop area X start coordinate
     */
    public int getCropX() {

        return m_cropX;
    }

    /**
     * Returns the crop area Y start coordinate.
     * <p>
     * 
     * Use {@link #setCropArea(int, int, int, int)} to set this value.
     * <p>
     * 
     * @return the crop area Y start coordinate
     */
    public int getCropY() {

        return m_cropY;
    }

    /**
     * Returns a new image scaler that is a down scale from the size of
     * <code>this</code> scaler
     * to the given scaler size.
     * <p>
     * 
     * If no down scale from this to the given scaler is required according to
     * {@link #isDownScaleRequired(ImageScaler)}, then <code>null</code> is
     * returned.
     * <p>
     * 
     * @param downScaler the image scaler that holds the down scaled target
     *            image dimensions
     * 
     * @return a new image scaler that is a down scale from the size of
     *         <code>this</code> scaler
     *         to the given target scaler size, or <code>null</code>
     */
    public ImageScaler getDownScaler(final ImageScaler downScaler) {

        if (!isDownScaleRequired(downScaler)) {
            // no down scaling is required
            return null;
        }

        int downHeight = downScaler.getHeight();
        int downWidth = downScaler.getWidth();

        final int height = getHeight();
        final int width = getWidth();

        if (((height > width) && (downHeight < downWidth))
            || ((width > height) && (downWidth < downHeight))) {
            // adjust orientation
            downHeight = downWidth;
            downWidth = downScaler.getHeight();
        }

        if (width > downWidth) {
            // width is too large, re-calculate height
            final float scale = (float)downWidth / (float)width;
            downHeight = Math.round(height * scale);
        } else if (height > downHeight) {
            // height is too large, re-calculate width
            final float scale = (float)downHeight / (float)height;
            downWidth = Math.round(width * scale);
        } else {
            // something is wrong, don't down scale
            return null;
        }

        // now create and initialize the result scaler
        return new ImageScaler(downScaler, downWidth, downHeight);
    }

    /**
     * Returns the list of image filter names (Strings) to be applied to the
     * image.
     * <p>
     * 
     * @return the list of image filter names (Strings) to be applied to the
     *         image
     */
    public List getFilters() {

        return m_filters;
    }

    /**
     * Returns the list of image filter names (Strings) to be applied to the
     * image as a String.
     * <p>
     * 
     * @return the list of image filter names (Strings) to be applied to the
     *         image as a String
     */
    public String getFiltersString() {

        final StringBuffer result = new StringBuffer();
        final Iterator i = m_filters.iterator();
        while (i.hasNext()) {
            final String filter = (String)i.next();
            result.append(filter);
            if (i.hasNext()) {
                result.append(':');
            }
        }
        return result.toString();
    }

    /**
     * Returns the height.
     * <p>
     * 
     * @return the height
     */
    public int getHeight() {

        return m_height;
    }

    /**
     * Returns the image type from the given file name based on the file suffix
     * (extension)
     * and the available image writers.
     * <p>
     * 
     * For example, for the file name "opencms.gif" the type is GIF, for
     * "opencms.jpg" is is "JPEG" etc.
     * <p>
     * 
     * In case the input filename has no suffix, or there is no known image
     * writer for the format defined by the suffix, <code>null</code> is
     * returned.
     * <p>
     * 
     * Any non-null result can be used if an image type input value is required.
     * <p>
     * 
     * @param filename the file name to get the type for
     * 
     * @return the image type from the given file name based on the suffix and
     *         the available image writers,
     *         or null if no image writer is available for the format
     */
    public String getImageType(final String filename) {

        return Simapi.getImageType(filename);
    }

    /**
     * Returns the maximum image size (width * height) to apply image blurring
     * when down scaling images.
     * <p>
     * 
     * Image blurring is required to achieve the best results for down scale
     * operations when the target image size is 2 times or more smaller then the
     * original image size. This parameter controls the maximum size (width *
     * height) of an image that is blurred before it is down scaled. If the
     * image is larger, no blurring is done. Image blurring is an expensive
     * operation in both CPU usage and memory consumption. Setting the blur size
     * to large may case "out of memory" errors.
     * <p>
     * 
     * @return the maximum image size (width * height) to apply image blurring
     *         when down scaling images
     */
    public int getMaxBlurSize() {

        return m_maxBlurSize;
    }

    /**
     * Returns the image pixel count, that is the image with multiplied by the
     * image height.
     * <p>
     * 
     * If this scaler is not valid (see {@link #isValid()}) the result is
     * undefined.
     * <p>
     * 
     * @return the image pixel count, that is the image with multiplied by the
     *         image height
     */
    public int getPixelCount() {

        return m_width * m_height;
    }

    /**
     * Returns the position.
     * <p>
     * 
     * @return the position
     */
    public int getPosition() {

        return m_position;
    }

    /**
     * Returns the image saving quality in percent (0 - 100).
     * <p>
     * 
     * This is used only if applicable, for example when saving JPEG images.
     * <p>
     * 
     * @return the image saving quality in percent
     */
    public int getQuality() {

        return m_quality;
    }

    /**
     * Returns the image rendering mode constant.
     * <p>
     * 
     * Possible values are:
     * <dl>
     * <dt>{@link Simapi#RENDER_QUALITY} (default)</dt>
     * <dd>Use best possible image processing - this may be slow sometimes.</dd>
     * 
     * <dt>{@link Simapi#RENDER_SPEED}</dt>
     * <dd>Fastest image processing but worse results - use this for thumbnails
     * or where speed is more important then quality.</dd>
     * 
     * <dt>{@link Simapi#RENDER_MEDIUM}</dt>
     * <dd>Use default rendering hints from JVM - not recommended since it's
     * almost as slow as the {@link Simapi#RENDER_QUALITY} mode.</dd>
     * </dl>
     * 
     * @return the image rendering mode constant
     */
    public int getRenderMode() {

        return m_renderMode;
    }

    /**
     * Returns a new image scaler that is a rescaler from <code>this</code>
     * scaler
     * size to the given target scaler size.
     * <p>
     * 
     * The height of the target image is calculated in proportion to the
     * original image width. If the width of the the original image is not
     * known, the target image width is calculated in proportion to the original
     * image height.
     * <p>
     * 
     * @param target the image scaler that holds the target image dimensions
     * 
     * @return a new image scaler that is a rescaler from the <code>this</code>
     *         scaler
     *         size to the given target scaler size
     */
    public ImageScaler getReScaler(final ImageScaler target) {

        int height = target.getHeight();
        int width = target.getWidth();

        if ((width > 0) && (getWidth() > 0)) {
            // width is known, calculate height
            final float scale = (float)width / (float)getWidth();
            height = Math.round(getHeight() * scale);
        } else if ((height > 0) && (getHeight() > 0)) {
            // height is known, calculate width
            final float scale = (float)height / (float)getHeight();
            width = Math.round(getWidth() * scale);
        } else if (isValid() && !target.isValid()) {
            // scaler is not valid but original is, so use original size of
            // image
            width = getWidth();
            height = getHeight();
        }

        if ((target.getType() == 1) && (!target.isValid())) {
            // "no upscale" has been requested, only one target dimension was
            // given
            if ((target.getWidth() > 0) && (getWidth() < width)) {
                // target width was given, target image should have this width
                height = getHeight();
            } else if ((target.getHeight() > 0) && (getHeight() < height)) {
                // target height was given, target image should have this height
                width = getWidth();
            }
        }

        // now create and initialize the result scaler
        return new ImageScaler(target, width, height);
    }

    /**
     * Returns the type.
     * <p>
     * 
     * Possible values are:
     * <dl>
     * 
     * <dt>0 (default): Scale to exact target size with background padding</dt>
     * <dd>
     * <ul>
     * <li>enlarge image to fit in target size (if required)
     * <li>reduce image to fit in target size (if required)
     * <li>keep image aspect ratio / proportions intact
     * <li>fill up with bgcolor to reach exact target size
     * <li>fit full image inside target size (only applies if reduced)
     * </ul>
     * </dd>
     * 
     * <dt>1: Thumbnail generation mode (like 0 but no image enlargement)</dt>
     * <dd>
     * <ul>
     * <li>dont't enlarge image
     * <li>reduce image to fit in target size (if required)
     * <li>keep image aspect ratio / proportions intact
     * <li>fill up with bgcolor to reach exact target size
     * <li>fit full image inside target size (only applies if reduced)
     * </ul>
     * </dd>
     * 
     * <dt>2: Scale to exact target size, crop what does not fit</dt>
     * <dd>
     * <ul>
     * <li>enlarge image to fit in target size (if required)
     * <li>reduce image to fit in target size (if required)
     * <li>keep image aspect ratio / proportions intact
     * <li>fit full image inside target size (crop what does not fit)
     * </ul>
     * </dd>
     * 
     * <dt>3: Scale and keep image proportions, target size variable</dt>
     * <dd>
     * <ul>
     * <li>enlarge image to fit in target size (if required)
     * <li>reduce image to fit in target size (if required)
     * <li>keep image aspect ratio / proportions intact
     * <li>scaled image will not be padded or cropped, so target size is likely
     * not the exact requested size
     * </ul>
     * </dd>
     * 
     * <dt>4: Don't keep image proportions, use exact target size</dt>
     * <dd>
     * <ul>
     * <li>enlarge image to fit in target size (if required)
     * <li>reduce image to fit in target size (if required)
     * <li>don't keep image aspect ratio / proportions intact
     * <li>the image will be scaled exactly to the given target size and likely
     * will be loose proportions
     * </ul>
     * </dd>
     * </dl>
     * 
     * @return the type
     */
    public int getType() {

        return m_type;
    }

    /**
     * Returns the width.
     * <p>
     * 
     * @return the width
     */
    public int getWidth() {

        return m_width;
    }

    /**
     * Returns a new image scaler that is a width based down scale from the size
     * of <code>this</code> scaler
     * to the given scaler size.
     * <p>
     * 
     * If no down scale from this to the given scaler is required because the
     * width of <code>this</code> scaler is not larger than the target width,
     * then the image dimensions of <code>this</code> scaler are unchanged in
     * the result scaler. No up scaling is done!
     * <p>
     * 
     * @param downScaler the image scaler that holds the down scaled target
     *            image dimensions
     * 
     * @return a new image scaler that is a down scale from the size of
     *         <code>this</code> scaler
     *         to the given target scaler size
     */
    public ImageScaler getWidthScaler(final ImageScaler downScaler) {

        int width = downScaler.getWidth();
        int height;

        if (getWidth() > width) {
            // width is too large, re-calculate height
            final float scale = (float)width / (float)getWidth();
            height = Math.round(getHeight() * scale);
        } else {
            // width is ok
            width = getWidth();
            height = getHeight();
        }

        // now create and initialize the result scaler
        return new ImageScaler(downScaler, width, height);
    }

    /**
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {

        return toString().hashCode();
    }

    /**
     * Returns <code>true</code> if all required parameters for image cropping
     * are available.
     * <p>
     * 
     * Required parameters are <code>"cx","cy"</code> (x, y start coordinate),
     * and <code>"ch","cw"</code> (crop height and width).
     * <p>
     * 
     * @return <code>true</code> if all required cropping parameters are
     *         available
     */
    public boolean isCropping() {

        return (m_cropX >= 0) && (m_cropY >= 0) && (m_cropHeigt > 0) && (m_cropWidth > 0);
    }

    /**
     * Returns <code>true</code> if this image scaler must be down scaled when
     * compared to the
     * given "down scale" image scaler.
     * <p>
     * 
     * If either <code>this</code> scaler or the given <code>downScaler</code>
     * is invalid according to {@link #isValid()}, then <code>false</code> is
     * returned.
     * <p>
     * 
     * The use case: <code>this</code> scaler represents an image (that is
     * contains width and height of an image). The <code>downScaler</code>
     * represents the maximum wanted image. The scalers are compared and if the
     * image represented by <code>this</code> scaler is too large,
     * <code>true</code> is returned. Image orientation is ignored, so for
     * example an image with 600x800 pixel will NOT be down scaled if the target
     * size is 800x600 but kept unchanged.
     * <p>
     * 
     * @param downScaler the down scaler to compare this image scaler with
     * 
     * @return <code>true</code> if this image scaler must be down scaled when
     *         compared to the
     *         given "down scale" image scaler
     */
    public boolean isDownScaleRequired(final ImageScaler downScaler) {

        if ((downScaler == null) || !isValid() || !downScaler.isValid()) {
            // one of the scalers is invalid
            return false;
        }

        if (getPixelCount() < (downScaler.getPixelCount() / 2)) {
            // the image has much less pixels then the target, so don't
            // downscale
            return false;
        }

        int downWidth = downScaler.getWidth();
        int downHeight = downScaler.getHeight();
        if (downHeight > downWidth) {
            // normalize image orientation - the width should always be the
            // large side
            downWidth = downHeight;
            downHeight = downScaler.getWidth();
        }
        int height = getHeight();
        int width = getWidth();
        if (height > width) {
            // normalize image orientation - the width should always be the
            // large side
            width = height;
            height = getWidth();
        }

        return (width > downWidth) || (height > downHeight);
    }

    /**
     * Returns <code>true</code> if all required parameters are available.
     * <p>
     * 
     * Required parameters are <code>"h"</code> (height), and <code>"w"</code>
     * (width).
     * <p>
     * 
     * @return <code>true</code> if all required parameters are available
     */
    public boolean isValid() {

        return (m_width > 0) && (m_height > 0);
    }

    /**
     * Parses the given parameters and sets the internal scaler variables
     * accordingly.
     * <p>
     * 
     * The parameter String must have a format like
     * <code>"h:100,w:200,t:1"</code>, that is a comma separated list of
     * attributes followed by a colon ":", followed by a value. As possible
     * attributes, use the constants from this class that start with
     * <code>SCALE_PARAM</Code> for example {@link #SCALE_PARAM_HEIGHT} or
     * {@link #SCALE_PARAM_WIDTH}.
     * <p>
     * 
     * @param parameters the parameters to parse
     */
    public void parseParameters(final String parameters) {

        m_width = -1;
        m_height = -1;
        m_position = 0;
        m_type = 0;
        m_color = Color.WHITE;
        m_cropX = -1;
        m_cropY = -1;
        m_cropWidth = -1;
        m_cropHeigt = -1;

        final String[] tokens = parameters.split(",");
        String k;
        String v;
        for (final String t : tokens) {
            // extract key and value
            k = null;
            v = null;
            final int idx = t.indexOf(':');
            if (idx >= 0) {
                k = t.substring(0, idx).trim();
                if (t.length() > idx) {
                    v = t.substring(idx + 1).trim();
                }
            }
            if ((k != null) && (v != null)) {
                // key and value are available
                if (ImageScaler.SCALE_PARAM_HEIGHT.equals(k)) {
                    // image height
                    m_height = ImageScaler.getIntValue(v, Integer.MIN_VALUE);
                } else if (ImageScaler.SCALE_PARAM_WIDTH.equals(k)) {
                    // image width
                    m_width = ImageScaler.getIntValue(v, Integer.MIN_VALUE);
                } else if (ImageScaler.SCALE_PARAM_CROP_X.equals(k)) {
                    // crop x coordinate
                    m_cropX = ImageScaler.getIntValue(v, Integer.MIN_VALUE);
                } else if (ImageScaler.SCALE_PARAM_CROP_Y.equals(k)) {
                    // crop y coordinate
                    m_cropY = ImageScaler.getIntValue(v, Integer.MIN_VALUE);
                } else if (ImageScaler.SCALE_PARAM_CROP_WIDTH.equals(k)) {
                    // crop width
                    m_cropWidth = ImageScaler.getIntValue(v, Integer.MIN_VALUE);
                } else if (ImageScaler.SCALE_PARAM_CROP_HEIGHT.equals(k)) {
                    // crop height
                    m_cropHeigt = ImageScaler.getIntValue(v, Integer.MIN_VALUE);
                } else if (ImageScaler.SCALE_PARAM_TYPE.equals(k)) {
                    // scaling type
                    setType(ImageScaler.getIntValue(v, -1));
                } else if (ImageScaler.SCALE_PARAM_COLOR.equals(k)) {
                    // image background color
                    setColor(v);
                } else if (ImageScaler.SCALE_PARAM_POS.equals(k)) {
                    // image position (depends on scale type)
                    setPosition(ImageScaler.getIntValue(v, -1));
                } else if (ImageScaler.SCALE_PARAM_QUALITY.equals(k)) {
                    // image position (depends on scale type)
                    setQuality(ImageScaler.getIntValue(v, 0));
                } else if (ImageScaler.SCALE_PARAM_RENDERMODE.equals(k)) {
                    // image position (depends on scale type)
                    setRenderMode(ImageScaler.getIntValue(v, 0));
                } else if (ImageScaler.SCALE_PARAM_FILTER.equals(k)) {
                    // image filters to apply
                    setFilters(v);
                }
            }
        }
        // initialize image crop area
        initCropArea();
    }

    /**
     * Returns a scaled version of the given image byte content according this
     * image scalers parameters.
     * <p>
     * 
     * @param content the image byte content to scale
     * @param filename the file name of the image, to get the image type from
     * 
     * @return a scaled version of the given image byte content according to the
     *         provided scaler parameters
     * @throws Exception
     */
    public byte[] scaleImage(final byte[] content, final String filename) throws Exception {

        byte[] result = content;

        // initialize image crop area
        initCropArea();

        RenderSettings renderSettings;
        if ((m_renderMode == 0) && (m_quality == 0)) {
            // use default render mode and quality
            renderSettings = new RenderSettings(Simapi.RENDER_QUALITY);
        } else {
            // use special render mode and/or quality
            renderSettings = new RenderSettings(m_renderMode);
            if (m_quality != 0) {
                renderSettings.setCompressionQuality(m_quality / 100f);
            }
        }
        // set max blur size
        renderSettings.setMaximumBlurSize(m_maxBlurSize);
        // new create the scaler
        final Simapi scaler = new Simapi(renderSettings);
        // calculate a valid image type supported by the imaging library (e.g.
        // "JPEG", "GIF")
        String imageType = Simapi.getImageType(filename);
        if (imageType == null) {
            // no MIME type found, use JPEG format to write images to the
            // cache
            imageType = "JPEG";
        }
        BufferedImage image = Simapi.read(content);

        if (isCropping()) {
            // check if the crop width / height are not larger then the
            // source image
            if ((m_cropHeigt > image.getHeight()) || (m_cropWidth > image.getWidth())) {
                // crop height / width is outside of image - return image
                // unchanged
                return result;
            }
        }

        Color color = getColor();

        if (!m_filters.isEmpty()) {
            final Iterator i = m_filters.iterator();
            while (i.hasNext()) {
                final String filter = (String)i.next();
                if (ImageScaler.FILTER_GRAYSCALE.equals(filter)) {
                    // add a gray scale filter
                    final GrayscaleFilter grayscaleFilter = new GrayscaleFilter();
                    renderSettings.addImageFilter(grayscaleFilter);
                } else if (ImageScaler.FILTER_SHADOW.equals(filter)) {
                    // add a drop shadow filter
                    final ShadowFilter shadowFilter = new ShadowFilter();
                    shadowFilter.setXOffset(5);
                    shadowFilter.setYOffset(5);
                    shadowFilter.setOpacity(192);
                    shadowFilter.setBackgroundColor(color.getRGB());
                    color = Simapi.COLOR_TRANSPARENT;
                    renderSettings.setTransparentReplaceColor(Simapi.COLOR_TRANSPARENT);
                    renderSettings.addImageFilter(shadowFilter);
                }
            }
        }

        if (isCropping()) {
            // image crop operation
            image = scaler.cropToSize(
                image,
                m_cropX,
                m_cropY,
                m_cropWidth,
                m_cropHeigt,
                getWidth(),
                getHeight(),
                color);
        } else {
            // image rescale operation
            switch (getType()) {
                // select the "right" method of scaling according to the "t"
                // parameter
                case 1:
                    // thumbnail generation mode (like 0 but no image
                    // enlargement)
                    image = scaler.resize(
                        image,
                        getWidth(),
                        getHeight(),
                        color,
                        getPosition(),
                        false);
                    break;
                case 2:
                    // scale to exact target size, crop what does not fit
                    image = scaler.resize(image, getWidth(), getHeight(), getPosition());
                    break;
                case 3:
                    // scale and keep image proportions, target size
                    // variable
                    image = scaler.resize(image, getWidth(), getHeight(), true);
                    break;
                case 4:
                    // don't keep image proportions, use exact target size
                    image = scaler.resize(image, getWidth(), getHeight(), false);
                    break;
                default:
                    // scale to exact target size with background padding
                    image = scaler.resize(
                        image,
                        getWidth(),
                        getHeight(),
                        color,
                        getPosition(),
                        true);
            }
        }

        if (!m_filters.isEmpty()) {
            final Rectangle targetSize = scaler.applyFilterDimensions(getWidth(), getHeight());
            image = scaler.resize(
                image,
                (int)targetSize.getWidth(),
                (int)targetSize.getHeight(),
                Simapi.COLOR_TRANSPARENT,
                Simapi.POS_CENTER);
            image = scaler.applyFilters(image);
        }

        // get the byte result for the scaled image
        result = scaler.getBytes(image, imageType);
        return result;
    }

    /**
     * Sets the color.
     * <p>
     * 
     * @param color the color to set
     */
    public void setColor(final Color color) {

        m_color = color;
    }

    /**
     * Sets the color as a String.
     * <p>
     * 
     * @param value the color to set
     */
    public void setColor(final String value) {

        if (ImageScaler.COLOR_TRANSPARENT.indexOf(value) == 0) {
            setColor(Simapi.COLOR_TRANSPARENT);
        } else {
            setColor(ImageScaler.getColorValue(value, Color.WHITE));
        }
    }

    /**
     * Sets the image crop area.
     * <p>
     * 
     * @param x the x coordinate for the crop
     * @param y the y coordinate for the crop
     * @param width the crop width
     * @param height the crop height
     */
    public void setCropArea(final int x, final int y, final int width, final int height) {

        m_cropX = x;
        m_cropY = y;
        m_cropWidth = width;
        m_cropHeigt = height;
    }

    /**
     * Sets the list of filters as a String.
     * <p>
     * 
     * @param value the list of filters to set
     */
    public void setFilters(final String value) {

        m_filters = new ArrayList();
        final String[] filters = value.split(":");
        for (String filter : filters) {
            filter = filter.trim().toLowerCase();
            final Iterator j = ImageScaler.FILTERS.iterator();
            while (j.hasNext()) {
                final String candidate = (String)j.next();
                if (candidate.startsWith(filter)) {
                    // found a matching filter
                    addFilter(candidate);
                    break;
                }
            }
        }
    }

    /**
     * Sets the height.
     * <p>
     * 
     * @param height the height to set
     */
    public void setHeight(final int height) {

        m_height = height;
    }

    /**
     * Sets the maximum image size (width * height) to apply image blurring when
     * downscaling images.
     * <p>
     * 
     * @param maxBlurSize the maximum image blur size to set
     * 
     * @see #getMaxBlurSize() for a more detailed description about this
     *      parameter
     */
    public void setMaxBlurSize(final int maxBlurSize) {

        m_maxBlurSize = maxBlurSize;
    }

    /**
     * Sets the scale position.
     * <p>
     * 
     * @param position the position to set
     */
    public void setPosition(final int position) {

        switch (position) {
            case Simapi.POS_DOWN_LEFT:
            case Simapi.POS_DOWN_RIGHT:
            case Simapi.POS_STRAIGHT_DOWN:
            case Simapi.POS_STRAIGHT_LEFT:
            case Simapi.POS_STRAIGHT_RIGHT:
            case Simapi.POS_STRAIGHT_UP:
            case Simapi.POS_UP_LEFT:
            case Simapi.POS_UP_RIGHT:
                // position is fine
                m_position = position;
                break;
            default:
                m_position = Simapi.POS_CENTER;
        }
    }

    /**
     * Sets the image saving quality in percent.
     * <p>
     * 
     * @param quality the image saving quality (in percent) to set
     */
    public void setQuality(final int quality) {

        if (quality < 0) {
            m_quality = 0;
        } else if (quality > 100) {
            m_quality = 100;
        } else {
            m_quality = quality;
        }
    }

    /**
     * Sets the image rendering mode constant.
     * <p>
     * 
     * @param renderMode the image rendering mode to set
     * 
     * @see #getRenderMode() for a list of allowed values for the rendering mode
     */
    public void setRenderMode(int renderMode) {

        if ((renderMode < Simapi.RENDER_QUALITY) || (renderMode > Simapi.RENDER_SPEED)) {
            renderMode = Simapi.RENDER_QUALITY;
        }
        m_renderMode = renderMode;
    }

    /**
     * Sets the scale type.
     * <p>
     * 
     * @param type the scale type to set
     * 
     * @see #getType() for a detailed description of the possible values for the
     *      type
     */
    public void setType(final int type) {

        if ((type < 0) || (type > 4)) {
            // invalid type, use 0
            m_type = 0;
        } else {
            m_type = type;
        }
    }

    /**
     * Sets the width.
     * <p>
     * 
     * @param width the width to set
     */
    public void setWidth(final int width) {

        m_width = width;
    }

    /**
     * Creates a request parameter configured with the values from this image
     * scaler, also
     * appends a <code>'?'</code> char as a prefix so that this may be directly
     * appended to an image URL.
     * <p>
     * 
     * This can be appended to an image request in order to apply image scaling
     * parameters.
     * <p>
     * 
     * @return a request parameter configured with the values from this image
     *         scaler
     */
    public String toRequestParam() {

        final StringBuffer result = new StringBuffer(128);
        result.append('?');
        result.append(ImageScaler.PARAM_SCALE);
        result.append('=');
        result.append(toString());

        return result.toString();
    }

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {

        if (m_scaleParameters != null) {
            return m_scaleParameters;
        }

        final StringBuffer result = new StringBuffer(64);
        if (isCropping()) {
            result.append(ImageScaler.SCALE_PARAM_CROP_X);
            result.append(':');
            result.append(m_cropX);
            result.append(',');
            result.append(ImageScaler.SCALE_PARAM_CROP_Y);
            result.append(':');
            result.append(m_cropY);
            result.append(',');
            result.append(ImageScaler.SCALE_PARAM_CROP_WIDTH);
            result.append(':');
            result.append(m_cropWidth);
            result.append(',');
            result.append(ImageScaler.SCALE_PARAM_CROP_HEIGHT);
            result.append(':');
            result.append(m_cropHeigt);
        }
        if (!isCropping() || ((m_width != m_cropWidth) || (m_height != m_cropHeigt))) {
            if (isCropping()) {
                result.append(',');
            }
            result.append(ImageScaler.SCALE_PARAM_WIDTH);
            result.append(':');
            result.append(m_width);
            result.append(',');
            result.append(ImageScaler.SCALE_PARAM_HEIGHT);
            result.append(':');
            result.append(m_height);
        }
        if (m_type > 0) {
            result.append(',');
            result.append(ImageScaler.SCALE_PARAM_TYPE);
            result.append(':');
            result.append(m_type);
        }
        if (m_position > 0) {
            result.append(',');
            result.append(ImageScaler.SCALE_PARAM_POS);
            result.append(':');
            result.append(m_position);
        }
        if (m_color != Color.WHITE) {
            result.append(',');
            result.append(ImageScaler.SCALE_PARAM_COLOR);
            result.append(':');
            result.append(getColorString());
        }
        if (m_quality > 0) {
            result.append(',');
            result.append(ImageScaler.SCALE_PARAM_QUALITY);
            result.append(':');
            result.append(m_quality);
        }
        if (m_renderMode > 0) {
            result.append(',');
            result.append(ImageScaler.SCALE_PARAM_RENDERMODE);
            result.append(':');
            result.append(m_renderMode);
        }
        if (!m_filters.isEmpty()) {
            result.append(',');
            result.append(ImageScaler.SCALE_PARAM_FILTER);
            result.append(':');
            result.append(getFiltersString());
        }
        m_scaleParameters = result.toString();
        return m_scaleParameters;
    }

    /**
     * Initializes the members with the default values.
     * <p>
     */
    private void init() {

        m_height = -1;
        m_width = -1;
        m_type = 0;
        m_position = 0;
        m_renderMode = 0;
        m_quality = 0;
        m_cropX = -1;
        m_cropY = -1;
        m_cropHeigt = -1;
        m_cropWidth = -1;
        m_color = Color.WHITE;
        m_filters = new ArrayList();
        m_maxBlurSize = ImageScaler.SCALE_DEFAULT_MAX_BLUR_SIZE;
    }

    /**
     * Initializes the crop area setting.
     * <p>
     * 
     * Only if all 4 required parameters have been set, the crop area is set
     * accordingly. Moreover, it is not required to specify the target image
     * width and height when using crop, because these parameters can be
     * calculated from the crop area.
     * <p>
     */
    private void initCropArea() {

        if (isCropping()) {
            // crop area is set up correctly
            // adjust target image height or width if required
            if (m_width < 0) {
                m_width = m_cropWidth;
            }
            if (m_height < 0) {
                m_height = m_cropHeigt;
            }
            // set type to 0 - scale type is ignored when using crop
            setType(0);
        }
    }

    /**
     * Copies all values from the given scaler into this scaler.
     * <p>
     * 
     * @param source the source scaler
     */
    private void initValuesFrom(final ImageScaler source) {

        m_width = source.m_width;
        m_height = source.m_height;
        m_type = source.m_type;
        m_position = source.m_position;
        m_renderMode = source.m_renderMode;
        m_quality = source.m_quality;
        m_color = source.m_color;
        m_filters = new ArrayList(source.m_filters);
        m_maxBlurSize = source.m_maxBlurSize;
        m_cropHeigt = source.m_cropHeigt;
        m_cropWidth = source.m_cropWidth;
        m_cropX = source.m_cropX;
        m_cropY = source.m_cropY;
    }
}